﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace PKTools.Database
{
    class SqlExpressionVisitor : ExpressionVisitor
    {
        readonly StringBuilder
            builder;
        readonly Dictionary<string, string>
            map;
        int
            inputID = 0;

        public static string Query(Dictionary<string, string> map, Expression exp)
        {
            var
                visitor = new SqlExpressionVisitor(map);

            using (new Stoper("visitor", () => visitor.ToString()))            
                visitor.Visit(exp);

            return visitor.ToString();
        }

        SqlExpressionVisitor(Dictionary<string, string> map)
        {
            this.builder = new StringBuilder();
            this.map = map;
        }

        bool IsNullConstant(Expression exp)
        {
            return exp.NodeType == ExpressionType.Constant && (exp as ConstantExpression).Value == null;
        }

        protected override Expression VisitBinary(BinaryExpression node)
        {
            this.builder.Append("(");
            this.Visit(node.Left);

            switch (node.NodeType)
            {
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                    this.builder.Append(" AND ");
                    break;
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    this.builder.Append(" OR ");
                    break;
                case ExpressionType.Equal:
                    if (IsNullConstant(node.Right))
                        this.builder.Append(" IS ");
                    else
                        this.builder.Append(" = ");
                    break;
                case ExpressionType.NotEqual:
                    if (IsNullConstant(node.Right))
                        this.builder.Append(" IS NOT ");
                    else
                        this.builder.Append(" != ");
                    break;
                case ExpressionType.LessThan:
                    this.builder.Append(" < ");
                    break;
                case ExpressionType.LessThanOrEqual:
                    this.builder.Append(" <= ");
                    break;
                case ExpressionType.GreaterThan:
                    this.builder.Append(" > ");
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    this.builder.Append(" >= ");
                    break;
                default:
                    throw new Exception("operator not supported: " + node.NodeType);
            }

            this.Visit(node.Right);
            this.builder.Append(")");

            return node;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.Expression != null)
            {
                switch (node.Expression.NodeType)
                {
                    case ExpressionType.Constant:
                    case ExpressionType.MemberAccess:
                        object
                            value = GetMemberConstant(node);

                        Constant(value, value.GetType());

                        return node;
                    case ExpressionType.Parameter:
                        this.builder.Append(Sql.Context.FormatColumn(this.map[node.Member.Name]));
                        return node;
                }
            }

            throw new Exception("member not supported: " + node.Member.Name);
        }

        void Constant(object value, Type type)
        {
            if (value == null || value == DBNull.Value)
                this.builder.Append("NULL");
            else
            {
                if (Type.GetTypeCode(type) == TypeCode.Object)
                    throw new Exception("constant not supported: " + value);

                var
                    input = "sev" + this.inputID++;

                Sql.Context.AddInput(input, value);
                this.builder.Append('@' + input);
            }
        }

        protected override Expression VisitConstant(ConstantExpression node)
        {
            if (node.Value as IQueryable == null)
                Constant(node.Value, node.Type);

            return node;
        }

        static object GetMemberConstant(Expression exp)
        {
            switch (exp.NodeType)
            {
                case ExpressionType.MemberAccess:
                    var
                        temp = exp as MemberExpression;
                    object
                        obj = GetMemberConstant(temp.Expression);
                    var
                        pi = temp.Member as PropertyInfo;
                    if (pi == null)
                    {
                        var
                            fi = temp.Member as FieldInfo;
                        if (fi == null)
                            throw new Exception("only property or field access supported: " + temp.Member.Name);

                        obj = fi.GetValue(obj);
                    }
                    else
                        obj = pi.GetValue(obj);

                    return obj;
                case ExpressionType.Constant:
                    return (exp as ConstantExpression).Value;
                default:
                    throw new Exception("node not supported: " + exp.NodeType);
            }
        }

        public override string ToString()
        {
            return this.builder.ToString();
        }
    }
}
